﻿using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Text;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Web;
using System.IO;
using System.Globalization;
using System.Text.RegularExpressions;
using System.Net;
using System.ComponentModel;

namespace Laharsub.Server.Wcf
{
    [ServiceContract]
    public class PubSubService
    {
        static byte[] newlineBuffer = Encoding.UTF8.GetBytes("\r\n");
        static MemoryStream emptyResponse = new MemoryStream();
        static Regex multiSubscribeParameterRegEx = new Regex(@"subs\[(\d+)\]\[(from|topicid)\]", RegexOptions.IgnoreCase);

        IPubSubBackend pubSubBackend = Laharsub.Backend;
        TimeSpan serverPollTimeout = TimeSpan.FromSeconds(Laharsub.GetParamAsInt("serverPollTimeoutInS"));

#if PARK
        [WebInvoke(Method = "GET", UriTemplate = "park"), OperationContract(AsyncPattern = true)]
        [Description(HttpAPIDocumentation.Park)]
        public IAsyncResult BeginPark(AsyncCallback callback, object state)
        {
            Console.Write("+");
            return new NeverAsyncResult { AsyncState = state };
        }

        public void EndPark(IAsyncResult result)
        {
            throw new InvalidOperationException();
        }

        class NeverAsyncResult : IAsyncResult
        {
            static System.Threading.ManualResetEvent waitHandle = new System.Threading.ManualResetEvent(false);
            public object AsyncState { get; set; }
            public System.Threading.WaitHandle AsyncWaitHandle { get { return waitHandle; } } 
            public bool CompletedSynchronously { get { return false; } } 
            public bool IsCompleted { get { return false; }  }
        }
#endif

        [WebInvoke(Method = "POST", UriTemplate = "topics"), OperationContract(AsyncPattern = true)]
        [Description(HttpAPIDocumentation.CreateTopic)]
        public IAsyncResult BeginCreateTopic(AsyncCallback callback, object state)
        {
            try
            {
                return this.pubSubBackend.BeginCreateTopic(callback, state);
            }
            catch (Exception e)
            {
                // TODO formal exception contract is needed
                throw new WebFaultException<string>(e.ToString(), HttpStatusCode.BadRequest);
            }
        }

        public Message EndCreateTopic(IAsyncResult result)
        {
            int topicId;
            try
            {
                topicId = this.pubSubBackend.EndCreateTopic(result);
#if DEBUG
                Console.WriteLine("CreateTopic: {0}", topicId);
#endif
            }
            catch (Exception e)
            {
                // TODO formal exception contract is needed
                throw new WebFaultException<string>(e.ToString(), HttpStatusCode.InternalServerError);
            }
            Message response = WebOperationContext.Current.CreateTextResponse(topicId.ToString(CultureInfo.InvariantCulture));
            return response;
        }

        [WebInvoke(Method = "POST", UriTemplate = "topics/{topicId}"), OperationContract(AsyncPattern = true)]
        [Description(HttpAPIDocumentation.Publish)]
        public IAsyncResult BeginPublish(string topicId, Stream body, AsyncCallback callback, object state)
        {
#if DEBUG
            Console.WriteLine("Publish: {0}", topicId);
#endif
            string contentType = WebOperationContext.Current.IncomingRequest.ContentType;
            if (string.IsNullOrWhiteSpace(contentType))
            {
                contentType = "text/plain";
            }
            int topicIdInt = 0;
            if (Int32.TryParse(topicId, out topicIdInt) && topicIdInt > 0)
            {
                try
                {
                    return this.pubSubBackend.BeginPublishMessage(topicIdInt, contentType, body, callback, state);
                }
                catch (Exception e)
                {
                    // TODO formal exception contract is needed
                    throw new WebFaultException<string>(e.ToString(), HttpStatusCode.BadRequest);
                }
            }
            else
            {
                throw new WebFaultException<string>("Unrecognized topic id. Topic ID must be a positive integer.", HttpStatusCode.BadRequest);
            }
        }

        public Message EndPublish(IAsyncResult result)
        {
            int messageId;
            try
            {
                messageId = this.pubSubBackend.EndPublishMessage(result);
            }
            catch (Exception e)
            {
                // TODO formal exception contract is needed
                throw new WebFaultException<string>(e.ToString(), HttpStatusCode.InternalServerError);
            }
            Message response = WebOperationContext.Current.CreateTextResponse(messageId.ToString(CultureInfo.InvariantCulture));
            return response;
        }

        [WebGet(UriTemplate = "subscriptions/volatile"), OperationContract(AsyncPattern = true)]
        [Description(HttpAPIDocumentation.Subscribe)]
        public IAsyncResult BeginVolatileSubscription(AsyncCallback callback, object state)
        {
#if DEBUG
            Console.WriteLine("Subscribe ({0}): {1}", DateTime.Now, Uri.UnescapeDataString(WebOperationContext.Current.IncomingRequest.UriTemplateMatch.RequestUri.Query));
#endif
            Dictionary<int, int> subs = this.ParseMultiSubscriptionParameters(WebOperationContext.Current.IncomingRequest.UriTemplateMatch.QueryParameters);
            try
            {
                return this.pubSubBackend.BeginSubscribe(subs, this.serverPollTimeout, callback, state);
            }
            catch (Exception e)
            {
                // TODO formal exception contract is needed
                throw new WebFaultException<string>(e.ToString(), HttpStatusCode.BadRequest);
            }
        }

        public Message EndVolatileSubscription(IAsyncResult result)
        {
            IEnumerable<PubSubMessage> messages;
            try
            {
                messages = this.pubSubBackend.EndSubscribe(result);
            }
            catch (Exception e)
            {
                // TODO formal exception contract is needed
                throw new WebFaultException<string>(e.ToString(), HttpStatusCode.InternalServerError);
            }
            if (messages == null)
            {
                return WebOperationContext.Current.CreateStreamResponse(emptyResponse, "text/plain");
            }
            else
            {
                string boundary = Guid.NewGuid().ToString().Replace("-", ".");
                Stream multipartStream = this.CreateMultipartStream("--" + boundary, messages);
                return WebOperationContext.Current.CreateStreamResponse(multipartStream, "multipart/mixed; boundary=" + boundary);
            }
        }

        // TODO Make this not create yet another copy of the data in memory (custom stream implementation required)        
        Stream CreateMultipartStream(string boundary, IEnumerable<PubSubMessage> messages)
        {
            MemoryStream result = new MemoryStream();
            byte[] buffer;

            try
            {
                foreach (PubSubMessage message in messages)
                {
                    buffer = Encoding.UTF8.GetBytes(string.Format(
                        CultureInfo.InvariantCulture,
                        "{0}\r\nContent-Type: {1}\r\nContent-Description: {2}/{3}\r\n\r\n",
                        boundary,
                        message.ContentType,
                        message.TopicId,
                        message.MessageId));
                    result.Write(buffer, 0, buffer.Length);
                    result.Write(message.Body, 0, message.Body.Length);
                    result.Write(newlineBuffer, 0, newlineBuffer.Length);
                }
                buffer = Encoding.UTF8.GetBytes(string.Format(
                    CultureInfo.InvariantCulture,
                    "{0}--",
                    boundary));
                result.Write(buffer, 0, buffer.Length);
                result.Seek(0, SeekOrigin.Begin);
            }
            catch (Exception)
            {
                result.Dispose();
                throw;
            }

            return result;
        }

        Dictionary<int, int> ParseMultiSubscriptionParameters(NameValueCollection queryParameters)
        {
            Dictionary<int, int> topicIds = new Dictionary<int, int>();
            Dictionary<int, int> froms = new Dictionary<int, int>();

            foreach (string p in queryParameters.Keys)
            {
                Match match = multiSubscribeParameterRegEx.Match(p);
                if (match.Success)
                {
                    string v = queryParameters[p];
                    int vint = 0;
                    if (int.TryParse(v, out vint) && vint > 0)
                    {
                        if ("from".Equals(match.Groups[2].Value, StringComparison.OrdinalIgnoreCase))
                        {
                            froms[Int32.Parse(match.Groups[1].Value)] = vint;
                        }
                        else
                        {
                            topicIds[Int32.Parse(match.Groups[1].Value)] = vint;
                        }
                    }
                    else
                    {
                        throw new WebFaultException<string>("Subscription parameter value must be a positive integer.", HttpStatusCode.BadRequest);
                    }
                }
            }

            Dictionary<int, int> result = new Dictionary<int, int>();
            foreach (int index in topicIds.Keys)
            {
                result[topicIds[index]] = froms.ContainsKey(index) ? froms[index] : 0;
            }
            if (result.Count == 0)
            {
                throw new WebFaultException<string>("At least one subscription must be specified.", HttpStatusCode.BadRequest);
            }
            
            return result;
        }
    }
}
